5.12. Рекомендации по разработке на Groovy
Рекомендации по разработке на Groovy
Введение в культуру кода Groovy
Groovy сочетает динамическую природу скриптовых языков с мощью платформы JVM. Культура кода в экосистеме Groovy строится на принципах читаемости, лаконичности и практической применимости. Язык предоставляет множество синтаксических упрощений по сравнению с Java, но это не означает отказ от дисциплины в проектировании. Качественный код на Groovy сохраняет структурированность, предсказуемость и удобство сопровождения при максимальном использовании выразительных возможностей языка.
Основные ценности разработки на Groovy:
- Читаемость превыше краткости — лаконичность не должна снижать понимание логики
- Использование идиом языка — применение замыканий, операторов расширения, безопасной навигации там, где это уместно
- Согласованность в рамках проекта — единые правила оформления для всех участников команды
- Прагматизм — выбор между динамической и статической типизацией на основе требований к надёжности и производительности
- Интеграция с экосистемой JVM — естественное взаимодействие с библиотеками и фреймворками на Java
Соглашения об именовании
Общие правила именования
Имена в коде на Groovy следуют стандартным соглашениям экосистемы JVM с учётом специфики языка:
- Классы и интерфейсы используют стиль
PascalCase:UserService,PaymentProcessor - Методы и переменные используют стиль
camelCase:calculateTotal,userList - Константы объявляются в стиле
SCREAMING_SNAKE_CASE:MAX_RETRY_COUNT,DEFAULT_TIMEOUT - Пакеты именуются строчными буквами без подчёркиваний:
com.example.payment - Файлы скриптов сохраняют имя класса с расширением
.groovy
Именование методов
Методы в Groovy часто выражают поведение более естественно, чем в Java. Рекомендуемые практики:
- Используйте глагольные префиксы для действий:
fetchUsers(),validateInput(),processOrder() - Для проверок применяйте префиксы
is,has,can:isActive(),hasPermission(),canExecute() - Методы, возвращающие коллекции, именуйте во множественном числе:
getActiveUsers(),findPendingOrders() - Для конвертаций используйте префиксы
toиas:toString(),asList(),toDate() - Методы-предикаты возвращают логическое значение и имеют осмысленное имя:
isEligibleForDiscount()
Groovy поддерживает методы с пробелами в имени через обратные кавычки, но такая практика применяется только в тестовых фреймворках (Spock):
def "should calculate total correctly when items present"() {
expect:
calculator.total([item1, item2]) == 42
}
Именование переменных
Переменные должны точно отражать своё назначение:
// Предпочтительно
def activeUsers = userRepository.findAllByStatus('ACTIVE')
def retryCount = 3
def maxConnectionPoolSize = 50
// Избегайте
def au = userRepository.findAllByStatus('ACTIVE')
def rc = 3
def m = 50
Для временных переменных в коротких замыканиях допустимы краткие имена:
def total = items.sum { it.price * it.quantity }
def names = users.collect { it.fullName }
Оформление кода
Отступы и пробелы
- Используйте 4 пробела для отступов, не применяйте символ табуляции
- После ключевых слов ставьте пробел:
if (condition),while (active),for (item in list) - Окружайте бинарные операторы пробелами:
a + b,count > 0,name == 'Groovy' - Не ставьте пробел после открывающей скобки и перед закрывающей:
method(arg1, arg2) - Ставьте пробел после запятых в списках и вызовах методов:
[1, 2, 3],process(item1, item2)
Фигурные скобки
Применяйте стиль K&R (Kernighan & Ritchie) для блоков кода:
if (user.active) {
process(user)
} else {
log.warn("Inactive user: ${user.name}")
}
class UserService {
def findAll() {
userRepository.list()
}
}
Для однострочных блоков в замыканиях допустимо опускать фигурные скобки:
users.each { println it.name }
def squares = (1..10).collect { it * it }
Переносы строк
При длинных выражениях применяйте вертикальное выравнивание:
def result = userRepository
.findByStatusAndRegion('ACTIVE', 'EUROPE')
.findAll { it.balance > MIN_BALANCE }
.sort { it.registrationDate }
.take(MAX_RESULTS)
Для цепочек методов каждый вызов размещайте на новой строке с отступом:
def users = User.createCriteria().list {
eq('status', 'ACTIVE')
ge('balance', 100)
order('lastName', 'asc')
order('firstName', 'asc')
maxResults(50)
}
Строковые литералы
Используйте подходящий тип строк в зависимости от задачи:
- Одинарные кавычки для простых строк:
'простая строка' - Двойные кавычки для интерполяции:
"Пользователь: ${user.name}" - Тройные кавычки для многострочных текстов:
def template = """
Здравствуйте, ${user.name}!
Ваш заказ №${order.id} обработан.
Сумма: ${order.total} руб.
""" - Обратные кавычки для многострочных строк без интерполяции или с минимальной обработкой
Структура проекта
Типичная структура проекта на Groovy соответствует стандартам Gradle/Maven:
project-root/
├── src/
│ ├── main/
│ │ ├── groovy/ # Основной код на Groovy
│ │ │ └── com/
│ │ │ └── example/
│ │ │ ├── domain/ # Сущности предметной области
│ │ │ ├── service/ # Сервисный слой
│ │ │ ├── repository/ # Работа с данными
│ │ │ └── util/ # Вспомогательные утилиты
│ │ ├── resources/ # Конфигурации, шаблоны, статические ресурсы
│ │ └── java/ # Интеграционный код на Java (при необходимости)
│ └── test/
│ ├── groovy/ # Тесты на Groovy
│ │ └── com/
│ │ └── example/
│ │ ├── service/
│ │ └── specification/ # Спецификации Spock
│ └── resources/ # Тестовые конфигурации и данные
├── build.gradle # Сборка Gradle
├── settings.gradle
└── gradle.properties # Свойства проекта
Для скриптовых проектов допустима упрощённая структура:
scripts/
├── utils/
│ ├── FileHelper.groovy
│ └── StringUtils.groovy
├── tasks/
│ ├── backup.groovy
│ └── migrate.groovy
└── config/
└── application.groovy
Проектирование классов и объектов
Принципы проектирования
Классы в Groovy должны соответствовать принципам SOLID с учётом динамической природы языка:
- Принцип единственной ответственности: класс решает одну задачу
- Открытость для расширения через метапрограммирование и категории
- Предпочитайте композицию наследованию
- Инкапсулируйте внутреннее состояние, предоставляя контролируемые точки доступа
Объявление классов
Используйте ключевое слово class для объявления обычных классов:
class User {
String firstName
String lastName
String email
Date registrationDate = new Date()
String getFullName() {
"$firstName $lastName"
}
boolean isActive() {
registrationDate > Date.parse('yyyy-MM-dd', '2020-01-01')
}
}
Для неизменяемых объектов применяйте @Immutable:
@Immutable
class Address {
String street
String city
String postalCode
String country
}
Для упрощённого объявления данных используйте @Canonical:
@Canonical
class Product {
String name
BigDecimal price
String category
}
Свойства и поля
Groovy автоматически генерирует геттеры и сеттеры для полей. Явное объявление геттеров/сеттеров требуется только для специальной логики:
class Account {
BigDecimal balance = 0
void deposit(BigDecimal amount) {
if (amount <= 0) throw new IllegalArgumentException("Сумма должна быть положительной")
balance += amount
}
boolean withdraw(BigDecimal amount) {
if (amount > balance) return false
balance -= amount
true
}
}
Для приватных полей используйте модификатор private:
class SecureService {
private String apiKey
SecureService(String key) {
this.apiKey = key?.trim() ?: throw new IllegalArgumentException("Ключ не может быть пустым")
}
String getMaskedKey() {
apiKey ? "${apiKey[0..3]}****${apiKey[-4..-1]}" : null
}
}
Интерфейсы и абстрактные классы
Интерфейсы объявляются стандартным образом:
interface PaymentGateway {
PaymentResult process(PaymentRequest request)
boolean supportsCurrency(String currency)
}
Абстрактные классы позволяют реализовать общую логику:
abstract class AbstractRepository<T> {
abstract List<T> findAll()
abstract T findById(Long id)
List<T> findAllById(Collection<Long> ids) {
ids.collect { findById(it) }.findAll()
}
}
Работа с коллекциями и функциональными конструкциями
Идиоматичная работа с коллекциями
Groovy предоставляет богатый набор методов для работы с коллекциями. Предпочитайте функциональные операции императивным циклам:
// Фильтрация
def activeUsers = users.findAll { it.status == 'ACTIVE' }
// Преобразование
def userEmails = users.collect { it.email }
// Агрегация
def totalBalance = accounts.sum { it.balance ?: 0 }
// Поиск
def admin = users.find { it.role == 'ADMIN' }
def hasPremium = users.any { it.plan == 'PREMIUM' }
def allVerified = users.every { it.emailVerified }
// Группировка
def usersByCountry = users.groupBy { it.country }
Замыкания
Замыкания в Groovy — основной инструмент для выразительного кода. Используйте краткий синтаксис:
// Полная форма
def square = { number -> number * number }
// Краткая форма с неявным параметром it
def isEven = { it % 2 == 0 }
// Многострочное замыкание
def process = { item ->
log.debug("Обработка: ${item.id}")
validator.validate(item)
repository.save(item)
notifier.send(item)
}
Для замыканий с несколькими параметрами указывайте их явно:
def comparator = { a, b -> a.priority <=> b.priority ?: a.name <=> b.name }
Операторы расширения
Операторы расширения позволяют добавлять методы к существующим классам без наследования:
// В файле StringExtensions.groovy
String.metaClass.toSlug = {
delegate.toLowerCase()
.replaceAll('[^a-z0-9]+', '-')
.replaceAll('(^-|-$)', '')
}
// Использование
assert 'Hello World!'.toSlug() == 'hello-world'
Для продакшн-кода предпочтительнее использовать категории или миксины для лучшего контроля области видимости.
Обработка ошибок и исключений
Стратегии обработки исключений
Обрабатывайте исключения осмысленно:
try {
paymentService.process(paymentRequest)
notificationService.sendConfirmation(user)
} catch (PaymentDeclinedException e) {
log.warn("Платёж отклонён для пользователя ${user.id}: ${e.message}")
userService.markPaymentFailed(user, e.reason)
} catch (ServiceUnavailableException e) {
log.error("Сбой платёжного шлюза", e)
retryService.scheduleRetry(paymentRequest, e)
throw new PaymentProcessingException("Временная недоступность сервиса", e)
} finally {
auditService.logAttempt(user, paymentRequest, System.currentTimeMillis() - startTime)
}
Безопасная навигация
Используйте оператор безопасной навигации ?. для работы с возможными null-значениями:
def city = user?.address?.city ?: 'Не указан'
def firstOrderDate = user?.orders?.min { it.date }?.date
Для цепочек вызовов с потенциальными null-значениями применяйте Elvis-оператор ?::
def displayName = user?.fullName ?: user?.email ?: 'Анонимный пользователь'
Валидация входных данных
Проверяйте входные параметры в публичных методах:
class OrderService {
Order createOrder(OrderRequest request) {
assert request != null, 'Запрос не может быть null'
assert request.items && !request.items.isEmpty(), 'Список товаров не может быть пустым'
assert request.customerId, 'Идентификатор клиента обязателен'
// Основная логика создания заказа
// ...
}
}
Для сложных сценариев валидации используйте специализированные библиотеки (например, Hibernate Validator).
Комментирование кода
Типы комментариев
Применяйте три типа комментариев в зависимости от назначения:
- Однострочные комментарии
//для кратких пояснений в коде - Многострочные комментарии
/* */для временного отключения блоков кода - Groovydoc-комментарии
/** */для документирования публичного API
Документирование классов и методов
Документируйте все публичные классы, методы и поля с помощью Groovydoc:
/**
* Сервис обработки платежей через внешние платёжные шлюзы.
* Поддерживает несколько провайдеров с автоматическим выбором
* на основе валюты и суммы платежа.
*/
class PaymentService {
/**
* Обрабатывает платёж через подходящий платёжный шлюз.
*
* @param request объект запроса с данными платежа
* @return результат обработки с идентификатором транзакции
* @throws PaymentValidationException при некорректных данных запроса
* @throws PaymentProcessingException при ошибках обработки платежа
*/
PaymentResult process(PaymentRequest request) {
// Реализация
}
}
Комментарии в коде
Размещайте комментарии на отдельной строке перед объясняемым кодом:
// Рассчитываем комиссию с учётом прогрессивной шкалы
def commission = calculateProgressiveCommission(amount)
// Применяем сезонную скидку только для премиум-пользователей
if (user.plan == 'PREMIUM' && isHolidaySeason()) {
applySeasonalDiscount(order)
}
Избегайте комментариев, дублирующих код:
// Плохо: комментарий повторяет очевидное
// Увеличиваем счётчик на единицу
counter++
// Хорошо: комментарий объясняет причину
// Счётчик увеличивается здесь, а не в основном цикле,
// чтобы избежать двойного учёта при повторной обработке
counter++
Тестирование кода на Groovy
Структура тестов
Используйте фреймворк Spock для написания выразительных спецификаций:
class UserServiceSpec extends Specification {
UserService userService
UserRepository userRepository = Mock()
def setup() {
userService = new UserService(userRepository: userRepository)
}
def "should return active users only"() {
given:
userRepository.findAll() >> [
new User(status: 'ACTIVE', name: 'Alice'),
new User(status: 'INACTIVE', name: 'Bob'),
new User(status: 'ACTIVE', name: 'Charlie')
]
when:
def result = userService.getActiveUsers()
then:
result.size() == 2
result*.name == ['Alice', 'Charlie']
}
def "should throw exception when user not found"() {
given:
userRepository.findById(123) >> null
when:
userService.getUser(123)
then:
thrown(UserNotFoundException)
}
}
Практики тестирования
- Тестируйте одно поведение в одном методе спецификации
- Используйте осмысленные имена методов тестов на естественном языке
- Применяйте разделы
given,when,thenдля структурирования тестов - Используйте моки и заглушки для изоляции тестируемого компонента
- Покрывайте граничные случаи и обработку ошибок
Интеграция с экосистемой JVM
Взаимодействие с кодом на Java
Groovy полностью совместим с Java. Вызывайте Java-код из Groovy без дополнительных преобразований:
// Использование Java-классов
import java.time.LocalDate
import java.util.stream.Collectors
def today = LocalDate.now()
def names = users.stream()
.filter { it.active }
.map { it.name }
.collect(Collectors.toList())
Для обратного вызова Groovy-кода из Java применяйте интерфейсы:
// Java-код
public interface DataProcessor {
void process(Map<String, Object> data);
}
// Groovy-реализация
class GroovyProcessor implements DataProcessor {
void process(Map<String, Object> data) {
// Обработка данных
}
}
Использование библиотек
Подключайте зависимости через Gradle:
dependencies {
implementation 'org.codehaus.groovy:groovy-all:3.0.19'
implementation 'io.micronaut:micronaut-inject-groovy:3.9.4'
implementation 'org.slf4j:slf4j-api:2.0.9'
testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0'
testImplementation 'org.spockframework:spock-junit4:2.3-groovy-3.0'
}
Статическая компиляция
Для критичных к производительности участков кода применяйте статическую компиляцию:
import groovy.transform.CompileStatic
@CompileStatic
class PerformanceCriticalService {
int calculate(int a, int b) {
a * b + (a - b)
}
}
Статическая компиляция обеспечивает производительность, близкую к Java, с сохранением синтаксических преимуществ Groovy.
Практические примеры архитектурных решений
Слой доступа к данным
@CompileStatic
class UserRepository {
private final Sql sql
UserRepository(DataSource dataSource) {
this.sql = new Sql(dataSource)
}
List<User> findAllActive() {
sql.rows('SELECT * FROM users WHERE status = ?', ['ACTIVE'])
.collect { rowToUser(it) }
}
User findById(Long id) {
def row = sql.firstRow('SELECT * FROM users WHERE id = ?', [id])
row ? rowToUser(row) : null
}
private User rowToUser(Map row) {
new User(
id: row.id as Long,
email: row.email as String,
name: row.name as String,
status: row.status as String,
createdAt: row.created_at as Timestamp
)
}
}
Сервисный слой с транзакционной поддержкой
@CompileStatic
class OrderService {
@Autowired
OrderRepository orderRepository
@Autowired
PaymentService paymentService
@Transactional
Order createOrder(OrderRequest request) {
validateRequest(request)
def order = new Order(
userId: request.userId,
items: request.items,
status: 'PENDING',
createdAt: new Date()
)
order = orderRepository.save(order)
processPayment(order, request.paymentDetails)
order.status = 'CONFIRMED'
orderRepository.save(order)
notificationService.sendOrderConfirmation(order)
order
}
private void validateRequest(OrderRequest request) {
assert request.userId, 'Идентификатор пользователя обязателен'
assert request.items && !request.items.isEmpty(), 'Список товаров не может быть пустым'
assert request.paymentDetails, 'Данные платежа обязательны'
}
private void processPayment(Order order, PaymentDetails details) {
try {
paymentService.charge(details, order.totalAmount)
} catch (PaymentException e) {
order.status = 'PAYMENT_FAILED'
orderRepository.save(order)
throw new OrderProcessingException('Ошибка обработки платежа', e)
}
}
}
Конфигурация приложения
// config/application.groovy
dataSource {
pooled = true
driverClassName = 'org.postgresql.Driver'
username = System.getenv('DB_USER') ?: 'app_user'
password = System.getenv('DB_PASSWORD') ?: 'secret'
url = System.getenv('DB_URL') ?: 'jdbc:postgresql://localhost:5432/app_db'
}
environments {
development {
dataSource {
url = 'jdbc:postgresql://localhost:5432/app_dev'
}
}
production {
dataSource {
properties {
maxActive = 50
maxIdle = 25
minIdle = 5
initialSize = 5
maxWait = 10000
}
}
}
}